﻿using System;
using Microsoft.Xna.Framework;

namespace Meteor.Resources
{
	public class ChaseCamera : Camera
	{
		/// <summary>
		/// Position of object being chased.
		/// </summary>
		public Vector3 ChasePosition
		{
			get { return chasePosition; }
			set { chasePosition = value; }
		}
		private Vector3 chasePosition;

		/// <summary>
		/// Direction the chased object is facing.
		/// </summary>
		public Vector3 ChaseDirection
		{
			get { return chaseDirection; }
			set { chaseDirection = value; }
		}
		private Vector3 chaseDirection;

		/// <summary>
		/// Chased object's Up vector.
		/// </summary>
		public Vector3 Up
		{
			get { return up; }
			set { up = value; }
		}
		private Vector3 up = Vector3.Up;

		/// <summary>
		/// Desired camera position in the chased object's coordinate system.
		/// </summary>
		public Vector3 DesiredPositionOffset
		{
			get { return desiredPositionOffset; }
			set { desiredPositionOffset = value; }
		}
		private Vector3 desiredPositionOffset = new Vector3(0, 2.0f, 2.0f);

		/// <summary>
		/// Desired camera position in world space.
		/// </summary>
		public Vector3 DesiredPosition
		{
			get
			{
				// Ensure correct value even if update has not been called this frame
				UpdateWorldPositions();

				return desiredPosition;
			}
		}
		private Vector3 desiredPosition;

		/// <summary>
		/// Look at point in the chased object's coordinate system.
		/// </summary>
		public Vector3 LookAtOffset
		{
			get { return lookAtOffset; }
			set { lookAtOffset = value; }
		}
		private Vector3 lookAtOffset = new Vector3(0, 2.8f, 0);

		/// <summary>
		/// Look at point in world space.
		/// </summary>
		public Vector3 LookAt
		{
			get
			{
				// Ensure correct value even if update has not been called this frame
				UpdateWorldPositions();

				return lookAt;
			}
		}
		private Vector3 lookAt;

		/// <summary>
		/// Physics coefficient which controls the influence of the camera's position
		/// over the spring force. The stiffer the spring, the closer it will stay to
		/// the chased object.
		/// </summary>
		public float Stiffness
		{
			get { return stiffness; }
			set { stiffness = value; }
		}
		private float stiffness = 18000.0f;

		/// <summary>
		/// Physics coefficient which approximates internal friction of the spring.
		/// Sufficient damping will prevent the spring from oscillating infinitely.
		/// </summary>
		public float Damping
		{
			get { return damping; }
			set { damping = value; }
		}
		private float damping = 6000.0f;

		/// <summary>
		/// Mass of the camera body. Heaver objects require stiffer springs with less
		/// damping to move at the same rate as lighter objects.
		/// </summary>
		public float Mass
		{
			get { return mass; }
			set { mass = value; }
		}
		private float mass = 500.0f;

		/// <summary>
		/// Velocity of camera.
		/// </summary>
		public Vector3 Velocity
		{
			get { return velocity; }
		}
		private Vector3 velocity;


		/// <summary>
		/// Perspective field of view.
		/// </summary>
		public float FieldOfView
		{
			get { return fieldOfView; }
			set { fieldOfView = value; }
		}
		private float fieldOfView = MathHelper.ToRadians(45.0f);

		/// <summary>
		/// Rebuilds object space values in world space. Invoke before publicly
		/// returning or privately accessing world space values.
		/// </summary>
		private void UpdateWorldPositions()
		{
			// Construct a matrix to transform from object space to worldspace
			Matrix transform = Matrix.Identity;
			transform.Forward = ChaseDirection;
			transform.Up = Up;
			transform.Right = Vector3.Cross(Up, ChaseDirection);

			// Calculate desired camera properties in world space
			desiredPosition = ChasePosition +
				Vector3.TransformNormal(DesiredPositionOffset, transform);
			lookAt = ChasePosition +
				Vector3.TransformNormal(LookAtOffset, transform);
		}

		/// <summary>
		/// Rebuilds camera's view and projection matricies.
		/// </summary>
		protected override void UpdateMatrices()
		{
			//view = Matrix.CreateLookAt(position, position + worldTransform.Forward, worldTransform.Up);
			view = Matrix.CreateLookAt(this.position, this.LookAt, this.Up);
			projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView,
				AspectRatio, nearPlaneDistance, farPlaneDistance);

			frustum.Matrix = view * projection;
		}

		/// <summary>
		/// Update the values to be chased by the camera
		/// </summary>
		public void UpdateChaseTarget(Vector3 position, Vector3 direction)
		{
			ChasePosition = position;
			ChaseDirection = direction;
			Up = Vector3.Up;
		}

		/// <summary>
		/// Forces camera to be at desired position and to stop moving. The is useful
		/// when the chased object is first created or after it has been teleported.
		/// Failing to call this after a large change to the chased object's position
		/// will result in the camera quickly flying across the world.
		/// </summary>
		public void Reset()
		{
			UpdateWorldPositions();

			// Stop motion
			velocity = Vector3.Zero;

			// Force desired position
			position = desiredPosition;

			UpdateMatrices();
		}

		/// <summary>
		/// Animates the camera from its current position towards the desired offset
		/// behind the chased object. The camera's animation is controlled by a simple
		/// physical spring attached to the camera and anchored to the desired position.
		/// </summary>
		public override void Update(GameTime gameTime)
		{
			if (gameTime == null)
				throw new ArgumentNullException("gameTime");

			UpdateWorldPositions();

			float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

			// Calculate spring force
			Vector3 stretch = position - desiredPosition;
			position -= stretch * elapsed * 10f;

			UpdateMatrices();
		}
	}
}
